Tìm hiểu cách triển khai React Error Boundaries với hooks để xử lý lỗi tải tài nguyên một cách mượt mà, cải thiện trải nghiệm người dùng và sự ổn định của ứng dụng.
Tải tài nguyên mạnh mẽ trong React: Làm chủ Error Boundaries với Hooks
Trong các ứng dụng web hiện đại, việc tải tài nguyên một cách bất đồng bộ là một thực hành phổ biến. Dù là tìm nạp dữ liệu từ API, tải hình ảnh, hay nhập các module, việc xử lý các lỗi tiềm ẩn trong quá trình tải tài nguyên là rất quan trọng để có một trải nghiệm người dùng mượt mà. React Error Boundaries cung cấp một cơ chế để bắt các lỗi JavaScript ở bất cứ đâu trong cây component con của chúng, ghi lại các lỗi đó, và hiển thị một giao diện người dùng dự phòng (fallback UI) thay vì làm sập toàn bộ ứng dụng. Bài viết này khám phá cách sử dụng Error Boundaries một cách hiệu quả kết hợp với React Hooks để quản lý các lỗi tải tài nguyên.
Tìm hiểu về Error Boundaries
Trước React 16, các lỗi JavaScript không được xử lý trong quá trình render component sẽ làm hỏng trạng thái nội bộ của React và gây ra các lỗi khó hiểu trong những lần render tiếp theo. Error Boundaries giải quyết vấn đề này bằng cách hoạt động như những khối bắt lỗi chung cho các lỗi xảy ra trong các component con của chúng. Chúng là các component React triển khai một hoặc cả hai phương thức vòng đời sau:
static getDerivedStateFromError(error): Phương thức tĩnh này được gọi sau khi một lỗi được ném ra bởi một component con. Nó nhận lỗi đã được ném ra làm đối số và trả về một giá trị để cập nhật trạng thái của component.componentDidCatch(error, info): Phương thức vòng đời này được gọi sau khi một lỗi được ném ra bởi một component con. Nó nhận lỗi đã được ném ra làm đối số, cũng như một đối tượng chứa thông tin về component nào đã ném ra lỗi. Bạn có thể sử dụng nó để ghi lại thông tin lỗi.
Quan trọng là, Error Boundaries chỉ bắt các lỗi trong giai đoạn rendering, trong các phương thức vòng đời, và trong các hàm khởi tạo của toàn bộ cây bên dưới chúng. Chúng không bắt lỗi cho:
- Trình xử lý sự kiện (tìm hiểu thêm trong phần bên dưới)
- Mã bất đồng bộ (ví dụ: các hàm callback của
setTimeouthoặcrequestAnimationFrame) - Kết xuất phía máy chủ (Server-side rendering)
- Lỗi được ném ra trong chính Error Boundary (thay vì trong các component con của nó)
Error Boundaries và React Hooks: Một sự kết hợp mạnh mẽ
Trong khi các class component thường được sử dụng để triển khai Error Boundaries, React Hooks cung cấp một cách tiếp cận hàm (functional) ngắn gọn và súc tích hơn. Chúng ta có thể tạo một hook useErrorBoundary có thể tái sử dụng để đóng gói logic xử lý lỗi và cung cấp một cách thuận tiện để bọc các component có thể ném ra lỗi trong quá trình tải tài nguyên.
Tạo một Hook useErrorBoundary tùy chỉnh
Đây là một ví dụ về hook useErrorBoundary:
import { useState, useCallback } from 'react';
function useErrorBoundary() {
const [error, setError] = useState(null);
const resetError = useCallback(() => {
setError(null);
}, []);
const captureError = useCallback((e) => {
setError(e);
}, []);
const ErrorBoundary = useCallback(({ children, fallback }) => {
if (error) {
return fallback ? fallback : An error occurred: {error.message || String(error)};
}
return children;
}, [error]);
return { ErrorBoundary, captureError, error, resetError };
}
export default useErrorBoundary;
Giải thích:
useState: Chúng tôi sử dụnguseStateđể quản lý trạng thái lỗi. Ban đầu, nó đặt lỗi lànull.useCallback: Chúng tôi sử dụnguseCallbackđể ghi nhớ (memoize) các hàmresetErrorvàcaptureError. Đây là một thực hành tốt để ngăn chặn các lần render lại không cần thiết nếu các hàm này được truyền xuống dưới dạng props.- Component
ErrorBoundary: Đây là một functional component được tạo bằnguseCallback, nhận vàochildrenvà một propfallbacktùy chọn. Nếu có lỗi trong trạng thái, nó sẽ render componentfallbackđược cung cấp hoặc một thông báo lỗi mặc định. Nếu không, nó sẽ render các children. Nó hoạt động như Error Boundary của chúng ta. Mảng phụ thuộc `[error]` đảm bảo nó sẽ render lại khi trạng thái `error` thay đổi. - Hàm
captureError: Hàm này được sử dụng để thiết lập trạng thái lỗi. Bạn sẽ gọi hàm này trong một khốitry...catchkhi tải tài nguyên. - Hàm
resetError: Hàm này xóa trạng thái lỗi, cho phép component render lại các children của nó (có thể thử lại việc tải tài nguyên).
Triển khai tải tài nguyên với xử lý lỗi
Bây giờ, hãy xem cách sử dụng hook này để xử lý lỗi tải tài nguyên. Hãy xem xét một component tìm nạp dữ liệu người dùng từ một API:
import React, { useState, useEffect } from 'react';
import useErrorBoundary from './useErrorBoundary';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const { ErrorBoundary, captureError, error, resetError } = useErrorBoundary();
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setUser(data);
} catch (e) {
captureError(e);
}
};
fetchData();
}, [userId, captureError]);
if (error) {
return (
Failed to load user data. {user.name}
Email: {user.email}
{/* Other user details */}Giải thích:
- Chúng tôi import hook
useErrorBoundary. - Chúng tôi gọi hook để lấy component
ErrorBoundary, hàmcaptureError, trạng tháierror, và hàmresetError. - Bên trong hook
useEffect, chúng tôi bọc lệnh gọi API trong một khốitry...catch. - Nếu một lỗi xảy ra trong quá trình gọi API, chúng tôi gọi
captureError(e)để thiết lập trạng thái lỗi. - Nếu trạng thái
errorđược thiết lập, chúng tôi render componentErrorBoundary. Chúng tôi cung cấp một propfallbacktùy chỉnh hiển thị thông báo lỗi và một nút "Thử lại". Nhấp vào nút sẽ gọiresetErrorđể xóa trạng thái lỗi, kích hoạt một lần render lại và một lần thử tìm nạp dữ liệu khác. - Nếu không có lỗi xảy ra và dữ liệu người dùng đã được tải, chúng tôi render chi tiết hồ sơ người dùng.
Xử lý các loại lỗi tải tài nguyên khác nhau
Các loại lỗi tải tài nguyên khác nhau có thể yêu cầu các chiến lược xử lý khác nhau. Dưới đây là một số kịch bản phổ biến và cách giải quyết chúng:
Lỗi mạng
Lỗi mạng xảy ra khi máy khách không thể kết nối với máy chủ (ví dụ: do mất mạng hoặc máy chủ ngừng hoạt động). Ví dụ trên đã xử lý các lỗi mạng cơ bản bằng cách sử dụng `response.ok`. Bạn có thể muốn thêm vào việc phát hiện lỗi phức tạp hơn, ví dụ:
//Inside the fetchData function
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
// Consider adding specific error code handling
if (response.status === 404) {
throw new Error("User not found");
} else if (response.status >= 500) {
throw new Error("Server error. Please try again later.");
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
}
const data = await response.json();
setUser(data);
} catch (error) {
if (error.message === 'Failed to fetch') {
// Likely a network error
captureError(new Error('Network error. Please check your internet connection.'));
} else {
captureError(error);
}
}
Trong trường hợp này, bạn có thể hiển thị một thông báo cho người dùng chỉ ra rằng có sự cố kết nối mạng và đề nghị họ kiểm tra kết nối internet của mình.
Lỗi API
Lỗi API xảy ra khi máy chủ trả về một phản hồi lỗi (ví dụ: 400 Bad Request hoặc 500 Internal Server Error). Như đã trình bày ở trên, bạn có thể kiểm tra `response.status` và xử lý các lỗi này một cách thích hợp.
Lỗi phân tích dữ liệu
Lỗi phân tích dữ liệu xảy ra khi phản hồi từ máy chủ không ở định dạng mong đợi và không thể được phân tích (ví dụ: JSON không hợp lệ). Bạn có thể xử lý các lỗi này bằng cách bọc lệnh gọi response.json() trong một khối try...catch:
//Inside the fetchData function
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setUser(data);
} catch (error) {
if (error instanceof SyntaxError) {
captureError(new Error('Failed to parse data from server.'));
} else {
captureError(error);
}
}
Lỗi tải hình ảnh
Đối với việc tải hình ảnh, bạn có thể sử dụng trình xử lý sự kiện onError trên thẻ <img>:
function MyImage({ src, alt }) {
const { ErrorBoundary, captureError } = useErrorBoundary();
const [imageLoaded, setImageLoaded] = useState(false);
const handleImageLoad = () => {
setImageLoaded(true);
};
const handleImageError = (e) => {
captureError(new Error(`Failed to load image: ${src}`));
};
return (
Failed to load image.